﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Windows.Foundation;
using Windows.Foundation.Collections;
using Windows.System;
using Windows.UI.Core;
using Windows.UI.ViewManagement;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Navigation;

namespace SettingsDemo.Common
{
  /// <summary>
  /// Typical implementation of Page that provides several important conveniences:
  /// <list type="bullet">
  /// <item>
  /// <description>Application view state to visual state mapping</description>
  /// </item>
  /// <item>
  /// <description>GoBack, GoForward, and GoHome event handlers</description>
  /// </item>
  /// <item>
  /// <description>Mouse and keyboard shortcuts for navigation</description>
  /// </item>
  /// <item>
  /// <description>State management for navigation and process lifetime management</description>
  /// </item>
  /// <item>
  /// <description>A default view model</description>
  /// </item>
  /// </list>
  /// </summary>
  [Windows.Foundation.Metadata.WebHostHidden]
  public class LayoutAwarePage : Page
  {
    /// <summary>
    /// Identifies the <see cref="DefaultViewModel"/> dependency property.
    /// </summary>
    public static readonly DependencyProperty DefaultViewModelProperty =
        DependencyProperty.Register("DefaultViewModel", typeof(IObservableMap<String, Object>),
        typeof(LayoutAwarePage), null);

    private List<Control> _layoutAwareControls;

    /// <summary>
    /// Initializes a new instance of the <see cref="LayoutAwarePage"/> class.
    /// </summary>
    public LayoutAwarePage()
    {
      if (Windows.ApplicationModel.DesignMode.DesignModeEnabled) return;

      // Create an empty default view model
      this.DefaultViewModel = new ObservableDictionary<String, Object>();

      // When this page is part of the visual tree make two changes:
      // 1) Map application view state to visual state for the page
      // 2) Handle keyboard and mouse navigation requests
      this.Loaded += (sender, e) =>
      {
        this.StartLayoutUpdates(sender, e);

        // Keyboard and mouse navigation only apply when occupying the entire window
        if (this.ActualHeight == Window.Current.Bounds.Height &&
            this.ActualWidth == Window.Current.Bounds.Width)
        {
          // Listen to the window directly so focus isn't required
          Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated +=
              CoreDispatcher_AcceleratorKeyActivated;
          Window.Current.CoreWindow.PointerPressed +=
              this.CoreWindow_PointerPressed;
        }
      };

      // Undo the same changes when the page is no longer visible
      this.Unloaded += (sender, e) =>
      {
        this.StopLayoutUpdates(sender, e);
        Window.Current.CoreWindow.Dispatcher.AcceleratorKeyActivated -=
            CoreDispatcher_AcceleratorKeyActivated;
        Window.Current.CoreWindow.PointerPressed -=
            this.CoreWindow_PointerPressed;
      };
    }

    /// <summary>
    /// An implementation of <see cref="IObservableMap&lt;String, Object&gt;"/> designed to be
    /// used as a trivial view model.
    /// </summary>
    protected IObservableMap<String, Object> DefaultViewModel
    {
      get
      {
        return this.GetValue(DefaultViewModelProperty) as IObservableMap<String, Object>;
      }

      set
      {
        this.SetValue(DefaultViewModelProperty, value);
      }
    }

    #region Navigation support

    /// <summary>
    /// Invoked as an event handler to navigate backward in the page's associated
    /// <see cref="Frame"/> until it reaches the top of the navigation stack.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the event.</param>
    protected virtual void GoHome(object sender, RoutedEventArgs e)
    {
      // Use the navigation frame to return to the topmost page
      if (this.Frame != null)
      {
        while (this.Frame.CanGoBack) this.Frame.GoBack();
      }
    }

    /// <summary>
    /// Invoked as an event handler to navigate backward in the navigation stack
    /// associated with this page's <see cref="Frame"/>.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the
    /// event.</param>
    protected virtual void GoBack(object sender, RoutedEventArgs e)
    {
      // Use the navigation frame to return to the previous page
      if (this.Frame != null && this.Frame.CanGoBack) this.Frame.GoBack();
    }

    /// <summary>
    /// Invoked as an event handler to navigate forward in the navigation stack
    /// associated with this page's <see cref="Frame"/>.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="e">Event data describing the conditions that led to the
    /// event.</param>
    protected virtual void GoForward(object sender, RoutedEventArgs e)
    {
      // Use the navigation frame to move to the next page
      if (this.Frame != null && this.Frame.CanGoForward) this.Frame.GoForward();
    }

    /// <summary>
    /// Invoked on every keystroke, including system keys such as Alt key combinations, when
    /// this page is active and occupies the entire window.  Used to detect keyboard navigation
    /// between pages even when the page itself doesn't have focus.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="args">Event data describing the conditions that led to the event.</param>
    private void CoreDispatcher_AcceleratorKeyActivated(CoreDispatcher sender,
        AcceleratorKeyEventArgs args)
    {
      var virtualKey = args.VirtualKey;

      // Only investigate further when Left, Right, or the dedicated Previous or Next keys
      // are pressed
      if ((args.EventType == CoreAcceleratorKeyEventType.SystemKeyDown ||
          args.EventType == CoreAcceleratorKeyEventType.KeyDown) &&
          (virtualKey == VirtualKey.Left || virtualKey == VirtualKey.Right ||
          (int)virtualKey == 166 || (int)virtualKey == 167))
      {
        var coreWindow = Window.Current.CoreWindow;
        var downState = CoreVirtualKeyStates.Down;
        bool menuKey = (coreWindow.GetKeyState(VirtualKey.Menu) & downState) == downState;
        bool controlKey = (coreWindow.GetKeyState(VirtualKey.Control) & downState) == downState;
        bool shiftKey = (coreWindow.GetKeyState(VirtualKey.Shift) & downState) == downState;
        bool noModifiers = !menuKey && !controlKey && !shiftKey;
        bool onlyAlt = menuKey && !controlKey && !shiftKey;

        if (((int)virtualKey == 166 && noModifiers) ||
            (virtualKey == VirtualKey.Left && onlyAlt))
        {
          // When the previous key or Alt+Left are pressed navigate back
          args.Handled = true;
          this.GoBack(this, new RoutedEventArgs());
        }
        else if (((int)virtualKey == 167 && noModifiers) ||
            (virtualKey == VirtualKey.Right && onlyAlt))
        {
          // When the next key or Alt+Right are pressed navigate forward
          args.Handled = true;
          this.GoForward(this, new RoutedEventArgs());
        }
      }
    }

    /// <summary>
    /// Invoked on every mouse click, touch screen tap, or equivalent interaction when this
    /// page is active and occupies the entire window.  Used to detect browser-style next and
    /// previous mouse button clicks to navigate between pages.
    /// </summary>
    /// <param name="sender">Instance that triggered the event.</param>
    /// <param name="args">Event data describing the conditions that led to the event.</param>
    private void CoreWindow_PointerPressed(CoreWindow sender,
        PointerEventArgs args)
    {
      var properties = args.CurrentPoint.Properties;

      // Ignore button chords with the left, right, and middle buttons
      if (properties.IsLeftButtonPressed || properties.IsRightButtonPressed ||
          properties.IsMiddleButtonPressed) return;

      // If back or foward are pressed (but not both) navigate appropriately
      bool backPressed = properties.IsXButton1Pressed;
      bool forwardPressed = properties.IsXButton2Pressed;
      if (backPressed ^ forwardPressed)
      {
        args.Handled = true;
        if (backPressed) this.GoBack(this, new RoutedEventArgs());
        if (forwardPressed) this.GoForward(this, new RoutedEventArgs());
      }
    }

    #endregion

    #region Visual state switching

    /// <summary>
    /// Invoked as an event handler, typically on the <see cref="FrameworkElement.Loaded"/>
    /// event of a <see cref="Control"/> within the page, to indicate that the sender should
    /// start receiving visual state management changes that correspond to application view
    /// state changes.
    /// </summary>
    /// <param name="sender">Instance of <see cref="Control"/> that supports visual state
    /// management corresponding to view states.</param>
    /// <param name="e">Event data that describes how the request was made.</param>
    /// <remarks>The current view state will immediately be used to set the corresponding
    /// visual state when layout updates are requested.  A corresponding
    /// <see cref="FrameworkElement.Unloaded"/> event handler connected to
    /// <see cref="StopLayoutUpdates"/> is strongly encouraged.  Instances of
    /// <see cref="LayoutAwarePage"/> automatically invoke these handlers in their Loaded and
    /// Unloaded events.</remarks>
    /// <seealso cref="DetermineVisualState"/>
    /// <seealso cref="InvalidateVisualState"/>
    public void StartLayoutUpdates(object sender, RoutedEventArgs e)
    {
      var control = sender as Control;
      if (control == null) return;
      if (this._layoutAwareControls == null)
      {
        // Start listening to view state changes when there are controls interested in updates
        Window.Current.SizeChanged += this.WindowSizeChanged;
        this._layoutAwareControls = new List<Control>();
      }
      this._layoutAwareControls.Add(control);

      // Set the initial visual state of the control
      VisualStateManager.GoToState(control, DetermineVisualState(ApplicationView.Value), false);
    }

    private void WindowSizeChanged(object sender, WindowSizeChangedEventArgs e)
    {
      this.InvalidateVisualState();
    }

    /// <summary>
    /// Invoked as an event handler, typically on the <see cref="FrameworkElement.Unloaded"/>
    /// event of a <see cref="Control"/>, to indicate that the sender should start receiving
    /// visual state management changes that correspond to application view state changes.
    /// </summary>
    /// <param name="sender">Instance of <see cref="Control"/> that supports visual state
    /// management corresponding to view states.</param>
    /// <param name="e">Event data that describes how the request was made.</param>
    /// <remarks>The current view state will immediately be used to set the corresponding
    /// visual state when layout updates are requested.</remarks>
    /// <seealso cref="StartLayoutUpdates"/>
    public void StopLayoutUpdates(object sender, RoutedEventArgs e)
    {
      var control = sender as Control;
      if (control == null || this._layoutAwareControls == null) return;
      this._layoutAwareControls.Remove(control);
      if (this._layoutAwareControls.Count == 0)
      {
        // Stop listening to view state changes when no controls are interested in updates
        this._layoutAwareControls = null;
        Window.Current.SizeChanged -= this.WindowSizeChanged;
      }
    }

    /// <summary>
    /// Translates <see cref="ApplicationViewState"/> values into strings for visual state
    /// management within the page.  The default implementation uses the names of enum values.
    /// Subclasses may override this method to control the mapping scheme used.
    /// </summary>
    /// <param name="viewState">View state for which a visual state is desired.</param>
    /// <returns>Visual state name used to drive the
    /// <see cref="VisualStateManager"/></returns>
    /// <seealso cref="InvalidateVisualState"/>
    protected virtual string DetermineVisualState(ApplicationViewState viewState)
    {
      return viewState.ToString();
    }

    /// <summary>
    /// Updates all controls that are listening for visual state changes with the correct
    /// visual state.
    /// </summary>
    /// <remarks>
    /// Typically used in conjunction with overriding <see cref="DetermineVisualState"/> to
    /// signal that a different value may be returned even though the view state has not
    /// changed.
    /// </remarks>
    public void InvalidateVisualState()
    {
      if (this._layoutAwareControls != null)
      {
        string visualState = DetermineVisualState(ApplicationView.Value);
        foreach (var layoutAwareControl in this._layoutAwareControls)
        {
          VisualStateManager.GoToState(layoutAwareControl, visualState, false);
        }
      }
    }

    #endregion

    #region Process lifetime management

    private String _pageKey;

    /// <summary>
    /// Invoked when this page is about to be displayed in a Frame.
    /// </summary>
    /// <param name="e">Event data that describes how this page was reached.  The Parameter
    /// property provides the group to be displayed.</param>
    protected override void OnNavigatedTo(NavigationEventArgs e)
    {
      // Returning to a cached page through navigation shouldn't trigger state loading
      if (this._pageKey != null) return;

      var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
      this._pageKey = "Page-" + this.Frame.BackStackDepth;

      if (e.NavigationMode == NavigationMode.New)
      {
        // Clear existing state for forward navigation when adding a new page to the
        // navigation stack
        var nextPageKey = this._pageKey;
        int nextPageIndex = this.Frame.BackStackDepth;
        while (frameState.Remove(nextPageKey))
        {
          nextPageIndex++;
          nextPageKey = "Page-" + nextPageIndex;
        }

        // Pass the navigation parameter to the new page
        this.LoadState(e.Parameter, null);
      }
      else
      {
        // Pass the navigation parameter and preserved page state to the page, using
        // the same strategy for loading suspended state and recreating pages discarded
        // from cache
        this.LoadState(e.Parameter, (Dictionary<String, Object>)frameState[this._pageKey]);
      }
    }

    /// <summary>
    /// Invoked when this page will no longer be displayed in a Frame.
    /// </summary>
    /// <param name="e">Event data that describes how this page was reached.  The Parameter
    /// property provides the group to be displayed.</param>
    protected override void OnNavigatedFrom(NavigationEventArgs e)
    {
      var frameState = SuspensionManager.SessionStateForFrame(this.Frame);
      var pageState = new Dictionary<String, Object>();
      this.SaveState(pageState);
      frameState[_pageKey] = pageState;
    }

    /// <summary>
    /// Populates the page with content passed during navigation.  Any saved state is also
    /// provided when recreating a page from a prior session.
    /// </summary>
    /// <param name="navigationParameter">The parameter value passed to
    /// <see cref="Frame.Navigate(Type, Object)"/> when this page was initially requested.
    /// </param>
    /// <param name="pageState">A dictionary of state preserved by this page during an earlier
    /// session.  This will be null the first time a page is visited.</param>
    protected virtual void LoadState(Object navigationParameter, Dictionary<String, Object> pageState)
    {
    }

    /// <summary>
    /// Preserves state associated with this page in case the application is suspended or the
    /// page is discarded from the navigation cache.  Values must conform to the serialization
    /// requirements of <see cref="SuspensionManager.SessionState"/>.
    /// </summary>
    /// <param name="pageState">An empty dictionary to be populated with serializable state.</param>
    protected virtual void SaveState(Dictionary<String, Object> pageState)
    {
    }

    #endregion

    /// <summary>
    /// Implementation of IObservableMap that supports reentrancy for use as a default view
    /// model.
    /// </summary>
    private class ObservableDictionary<K, V> : IObservableMap<K, V>
    {
      private class ObservableDictionaryChangedEventArgs : IMapChangedEventArgs<K>
      {
        public ObservableDictionaryChangedEventArgs(CollectionChange change, K key)
        {
          this.CollectionChange = change;
          this.Key = key;
        }

        public CollectionChange CollectionChange { get; private set; }
        public K Key { get; private set; }
      }

      private Dictionary<K, V> _dictionary = new Dictionary<K, V>();
      public event MapChangedEventHandler<K, V> MapChanged;

      private void InvokeMapChanged(CollectionChange change, K key)
      {
        var eventHandler = MapChanged;
        if (eventHandler != null)
        {
          eventHandler(this, new ObservableDictionaryChangedEventArgs(CollectionChange.ItemInserted, key));
        }
      }

      public void Add(K key, V value)
      {
        this._dictionary.Add(key, value);
        this.InvokeMapChanged(CollectionChange.ItemInserted, key);
      }

      public void Add(KeyValuePair<K, V> item)
      {
        this.Add(item.Key, item.Value);
      }

      public bool Remove(K key)
      {
        if (this._dictionary.Remove(key))
        {
          this.InvokeMapChanged(CollectionChange.ItemRemoved, key);
          return true;
        }
        return false;
      }

      public bool Remove(KeyValuePair<K, V> item)
      {
        V currentValue;
        if (this._dictionary.TryGetValue(item.Key, out currentValue) &&
            Object.Equals(item.Value, currentValue) && this._dictionary.Remove(item.Key))
        {
          this.InvokeMapChanged(CollectionChange.ItemRemoved, item.Key);
          return true;
        }
        return false;
      }

      public V this[K key]
      {
        get
        {
          return this._dictionary[key];
        }
        set
        {
          this._dictionary[key] = value;
          this.InvokeMapChanged(CollectionChange.ItemChanged, key);
        }
      }

      public void Clear()
      {
        var priorKeys = this._dictionary.Keys.ToArray();
        this._dictionary.Clear();
        foreach (var key in priorKeys)
        {
          this.InvokeMapChanged(CollectionChange.ItemRemoved, key);
        }
      }

      public ICollection<K> Keys
      {
        get { return this._dictionary.Keys; }
      }

      public bool ContainsKey(K key)
      {
        return this._dictionary.ContainsKey(key);
      }

      public bool TryGetValue(K key, out V value)
      {
        return this._dictionary.TryGetValue(key, out value);
      }

      public ICollection<V> Values
      {
        get { return this._dictionary.Values; }
      }

      public bool Contains(KeyValuePair<K, V> item)
      {
        return this._dictionary.Contains(item);
      }

      public int Count
      {
        get { return this._dictionary.Count; }
      }

      public bool IsReadOnly
      {
        get { return false; }
      }

      public IEnumerator<KeyValuePair<K, V>> GetEnumerator()
      {
        return this._dictionary.GetEnumerator();
      }

      System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator()
      {
        return this._dictionary.GetEnumerator();
      }

      public void CopyTo(KeyValuePair<K, V>[] array, int arrayIndex)
      {
        int arraySize = array.Length;
        foreach (var pair in this._dictionary)
        {
          if (arrayIndex >= arraySize) break;
          array[arrayIndex++] = pair;
        }
      }
    }
  }
}
